package hu.elte.inf.holiuml.model;

import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.io.Serializable;
import java.util.Vector;
import hu.elte.inf.holiuml.coordgeom.*;
import hu.elte.inf.holiuml.fileutil.FileUtilFactory;

/**
 * @author Csaba Hoch
 */
abstract public class Node implements Drawable, Serializable {
	
	public Node() {
	}
	
	/**
	 * The horizontal coordinate of the top left corner of the node.
	 */
	protected int x = 0;

	/**
	 * The vertical coordinate of the top left corner of the node.
	 */
	protected int y = 0;

	/**
	 * The width of the node in pixels. Calculated by the recalculate_size
	 * method.
	 */
	protected int width = 0;

	/**
	 * The height of the node in pixels. Calculated by the recalculateSize
	 * method.
	 */
	protected int height = 0;

	/**
	 * This variable stores the lines that will be drawn when the node is drawn.
	 * The first element of the lines variable will be the top line of the node.
	 * Every line is either a proper string or null. If a line is null, it means
	 * that a horizontal line should be drawn. Otherwise, the string itself
	 * should be drawn. This variable is refreshed by the recalculate_lines
	 * method, which is implemented in the subclasses of Node.
	 */
	protected Vector<String> lines;

	/**
	 * The height of one line text in pixels. Calculated by the recalculateSize
	 * method.
	 */
	protected int lineHeight = 0;

	/**
	 * The maximal ascent of the text in pixels. (I.e. the maximal distance
	 * between the top line and base line of the text.) Calculated by the
	 * recalculateSize method.
	 */
	protected int maxAscent = 0;

	/**
	 * It stores whether the variables that store that size information (width,
	 * height, lineHeight, maxAscent) and the lines variable are up-to-date.
	 */
	protected boolean sizeUpToDate = false;

	/**
	 * Returns whether the given point is inside the node on the canvas.
	 * 
	 * @param p
	 *            The given point.
	 */
	public boolean inside(Point p) {
		return (x <= p.x && p.x <= x + getWidth() && y <= p.y && p.y <= y
				+ getHeight());
	}

	/**
	 * Recalculates the lines variable, which contains the lines that will be
	 * drawn.
	 */
	abstract protected void recalculateLines(Graphics g);

	/**
	 * Recalculates the size information (width, height, lineHeight, maxAscent)
	 * and the lines variable.
	 */
	protected void recalculateSize(Graphics g) {
		lines = new Vector<String>();
		recalculateLines(g);
		
		TextUtils tu = new TextUtils(g);
		lineHeight = tu.lineHeight();
		maxAscent = tu.maxAscent();
		
		int textWidth = 0;
		height = 2 * MARGIN;
		for (String line : lines) {
			if (line != null) {
				textWidth = Math.max(textWidth, tu.stringWidth(line));
			}
			height += lineHeight;
		}
		width = textWidth + 2 * MARGIN;
	}

	/**
	 * Draws the node. First it recalculates the size information, if needed.
	 * Then it draws the bounds and all the lines.
	 */
	public void draw(Graphics g) {

		if (!sizeUpToDate) {
			recalculateSize(g);
		}

		g.setColor(java.awt.Color.WHITE);
		g.fillRect(getX() + 1, getY() + 1, getWidth() - 1, getHeight() - 1);
		drawBounds(g);

		g.setColor(java.awt.Color.BLACK);
		int x = getX();
		int y = getY() + MARGIN;
		for (String str : lines) {
			if (str != null) {
				g.drawString(str, x + MARGIN, y + maxAscent);
			} else {
				g.drawLine(x, y + (lineHeight / 2), x + width, y
						+ (lineHeight / 2));
			}
			y += lineHeight;
		}
	}

	/**
	 * Draws the bounds of the node.
	 */
	protected void drawBounds(Graphics g) {
		g.setColor(java.awt.Color.BLACK);
		g.drawRect(getX(), getY(), getWidth(), getHeight());
	}

	public enum HorizontalDirection {
		LEFT, CENTER, RIGHT
	}

	public enum VerticalDirection {
		TOP, CENTER, BOTTOM
	}

	public enum Direction {
		NORTH, SOUTH, EAST, WEST
	}

	/**
	 * Gets a special point of the Node.
	 */
	public Point getPoint(HorizontalDirection h, VerticalDirection v) {
		int x2 = (h == HorizontalDirection.LEFT ? x
				: h == HorizontalDirection.CENTER ? x + width / 2 : x + width);
		int y2 = (v == VerticalDirection.TOP ? y
				: v == VerticalDirection.CENTER ? y + height / 2 : y + height);
		return new Point(x2, y2);
	}
	
	public Point getPoint(Direction d) {
		if (d==Direction.NORTH) {
			return getPoint(HorizontalDirection.CENTER, VerticalDirection.TOP);
		} else if (d==Direction.SOUTH) {
			return getPoint(HorizontalDirection.CENTER, VerticalDirection.BOTTOM);
		} else if (d==Direction.WEST) {
			return getPoint(HorizontalDirection.LEFT, VerticalDirection.CENTER);
		} else {
			return getPoint(HorizontalDirection.RIGHT, VerticalDirection.CENTER);
		} 
	}

	//	 p1 +<----+ p2
	//	    |     ^
	//	    |     |
	//	    |     |
	//	    |     |
	//	    V     |
	//	 p3 +---->+ p4
	public Line getSideLine(Direction d) {
		CGPoint p1 = new CGPoint(getPoint(HorizontalDirection.LEFT,
				VerticalDirection.TOP));
		CGPoint p2 = new CGPoint(getPoint(HorizontalDirection.RIGHT,
				VerticalDirection.TOP));
		CGPoint p3 = new CGPoint(getPoint(HorizontalDirection.LEFT,
				VerticalDirection.BOTTOM));
		CGPoint p4 = new CGPoint(getPoint(HorizontalDirection.RIGHT,
				VerticalDirection.BOTTOM));

		if (d==Direction.NORTH) {
			return new Line(p2, p1);
		} else if (d==Direction.SOUTH) {
			return new Line(p3, p4);
		} else if (d==Direction.WEST) {
			return new Line(p1, p3);
		} else {
			return new Line(p4, p2);
		} 
	}

	//		   l1            l2
	//		     \           /
	//		      \     left/right
	//		  left \right  /
	//		     p1 +-----+ p2
	//		        |\   /|
	//		        | \ / |
	//		        |  *  |
	//		        | / \ |
	//		        |/   \|
	//		     p3 +-----+ p4
	//		       /       \
	public Direction getSpecDirection(Point p) {
		CGPoint cp = new CGPoint(p);
		CGPoint p1 = new CGPoint(getPoint(HorizontalDirection.LEFT,
				VerticalDirection.TOP));
		CGPoint p2 = new CGPoint(getPoint(HorizontalDirection.RIGHT,
				VerticalDirection.TOP));
		CGPoint p3 = new CGPoint(getPoint(HorizontalDirection.LEFT,
				VerticalDirection.BOTTOM));
		CGPoint p4 = new CGPoint(getPoint(HorizontalDirection.RIGHT,
				VerticalDirection.BOTTOM));
		Line l1 = new Line(p4, p1);
		Line l2 = new Line(p3, p2);
		if (l1.isOnTheLeft(cp) && l2.isOnTheLeft(cp)) {
			return Direction.WEST;
		} else if (l1.isOnTheRight(cp) && l2.isOnTheLeft(cp)) {
			return Direction.NORTH;
		} else if (l1.isOnTheRight(cp) && l2.isOnTheRight(cp)) {
			return Direction.EAST;
		} else {
			return Direction.SOUTH;
		}
	}
	
	public Point getSpecPoint(Point p) {
		return getPoint(getSpecDirection(p));
	}

	/**
	 * Moves the node with an (x,y) vector.
	 */
	public void move(int x, int y) {
		this.x += x;
		this.y += y;
	}

	public int getX() {
		return this.x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return this.y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public int getNodeCenterX() {
		return getX() + getWidth() / 2;
	}

	public int getNodeCenterY() {
		return getY() + getHeight() / 2;
	}
	
	/*
	private String xmlId;
	
	public void setNodeId(String xmlId) {
		this.xmlId = xmlId;
	}

	@javax.xml.bind.annotation.XmlID
	public String getNodeId() {
		if(xmlId == null) {
			setNodeId(FileUtilFactory.getNextId());
		}
		
		return xmlId;
	}
	*/
}
